//microcommand define -lnn-stroke-bitmap
const Direction = {
  Down: 1,
  Right: 2,
  Up: 4,
  Left: 8,
  None: 0,
}
const clockwise = [
  Direction.Right,
  Direction.Down,
  Direction.Left,
  Direction.Up,
]

function trace(grid, connectDiagonal = false) {
  const rows = grid.length + 1
  const cols = grid[0].length + 1
  const dirOrder = connectDiagonal ? [-1, 0, 1] : [1, 0, -1]

  const edges = Array.from(
    { length: rows },
    (_, row) => Array.from(
      { length: cols },
      (_, col) => {
        const curr = grid[row]?.[col]
        const top = grid[row - 1]?.[col]
        const left = grid[row]?.[col - 1]
        const topLeft = grid[row - 1]?.[col - 1]
        return (
          (left && !curr && Direction.Down) |
          (curr && !top && Direction.Right) |
          (top && !topLeft && Direction.Up) |
          (topLeft && !left && Direction.Left)
        )
      },
    ),
  )

  const paths = []

  // scan for edges going right
  for (let startRow = 0; startRow < rows; startRow++) {
    for (let startCol = 0; startCol < cols; startCol++) {
      let row = startRow
      let col = startCol
      const startDir = Direction.Right
      if (!(edges[row][col] & startDir)) continue

      // follow along the path
      let dir = startDir
      let lastDir = Direction.None
      const path = [col, row]
      do {
        // follow edge onto next point
        switch (dir) {
          case Direction.Down: row++; break
          case Direction.Right: col++; break
          case Direction.Up: row--; break
          case Direction.Left: col--; break
        }
        // append move to path
        if (dir === lastDir) path.pop()
        path.push(path.length % 2 === 0 ? col : row)
        lastDir = dir

        // find next edge
        const dirIndex = clockwise.indexOf(dir)
        const tryDirs = dirOrder.map(n => clockwise.at((dirIndex + n) % 4))
        dir = tryDirs.find(dir => edges[row][col] & dir)

        // erase edge from graph
        edges[row][col] &= ~dir
      } while (row !== startRow || col !== startCol || dir !== startDir)

      path.pop()
      path.pop()
      paths.push(path)
    }
  }
  return paths
}

function formatPaths(paths) {
  return paths.map(p => [
    `M${p.slice(0, 2).join(",")}`,
    ...[...p.slice(2), p[0]].map((n, i) => `${i % 2 === 0 ? "H" : "V"}${n}`),
    "Z",
  ].join("")).join("")
}

function coordsAt(i, path) {
  return [
    path.at((i & -2) % path.length),
    path.at((((i + 1) & -2) - 1) % path.length),
  ]
}

function formatPathsRounded(paths, radius) {
  return paths.map(path => {
    const out = []
    let lastX = NaN
    let lastY = NaN
    for (let i = 0; i < path.length; ++i) {
      // angle APB
      const [xA, yA] = coordsAt(i - 1, path)
      const [x, y] = coordsAt(i, path)
      const [xB, yB] = coordsAt(i + 1, path)
      // vector a = (dx1, dy1), b = (dx2, dy2)
      const dx1 = xA - x
      const dy1 = yA - y
      const dx2 = xB - x
      const dy2 = yB - y
      // spline A′B′ endpoints
      const x1 = x + Math.sign(dx1) * radius
      const y1 = y + Math.sign(dy1) * radius
      const x2 = x + Math.sign(dx2) * radius
      const y2 = y + Math.sign(dy2) * radius

      if (i === 0) out.push(`M${[x1, y1]}`)
      else if (x1 !== lastX || y1 !== lastY)
        out.push(i % 2 === 0 ? `H${x1}` : `V${y1}`)
      //out.push(`Q${[x, y, x2, y2]}`)
      // clockwise arc when a × b < 0
      const sweep = +(dx1 * dy2 - dx2 * dy1 < 0)
      out.push(`A${[radius, radius, 0, 0, sweep, x2, y2]}`)
      lastX = x2
      lastY = y2
    }
    out.push("Z")
    return out.join("")
  }).join("")
}

const defaultFunction = '<0.5÷⧻⟜/+↙3°⍉'

inject(["uiua"])
signature("<image:image>", { checkUnknown: true })
option("function", "-f <'uiua-code'>", { params: { default: defaultFunction } })
option("scale", "-s <posint:posint>", { fallback: 1 })
option("radius", "-r <number:number>", { fallback: 0 })
before(({ session, options }) => {
  if (options.radius < options.scale / -2 || options.radius > options.scale / 2) {
    session.send(session.i18n(".invalid-radius"))
    return []
  }
})
action(async ({ session, options }, image) => {
  const uiuaCode =
    (image ? '◌°img' : '') + '\n' +
    (options.function || defaultFunction) + '\n' +
    '⍤"Resulting array is not of rank 2" =2⧻⊸△\n' +
    '⍤"Resulting array contains value(s) other than 0 and 1" /×∊0_1⊸♭\n' +
    '&p json\n'
  let uiuaResult
  try {
    const imageBytes = image && await ctx.uiua.readInput("img", image)
    uiuaResult = await ctx.uiua.eval(uiuaCode, {
      inputs: imageBytes ? [imageBytes] : [],
      experimental: true,
      timeout: 8000,
    })
  } catch (err) {
    if (err.name === "UiuaError")
      return void session.send(session.i18n("commands.uiua.messages." + err.message))
    throw err
  }
  if (uiuaResult.error) return void session.send(session.i18n(".parse-error", [uiuaResult.error]))
  const bitmap = JSON.parse(new TextDecoder().decode(uiuaResult.stdout))
  let paths = trace(bitmap, options.radius < 0 || Object.is(-0, options.radius))
  if (options.scale !== 1) paths = paths.map(p => p.map(n => n * options.scale))
  if (options.radius === 0) return formatPaths(paths)
  return formatPathsRounded(paths, Math.abs(options.radius))
})

locale("zh-CN", {
  description: "将点阵图转换为矢量路径",
  arguments: {
    image: "图片",
  },
  options: {
    function: "二值化图片的 Uiua 函数（默认：{default}）",
    scale: "缩放倍率",
    radius: "圆角半径（必须在 scale 的正负二分之一之间〔含〕；负数〔包括 -0〕表示使对角线方向相邻的方格连接）",
  },
  messages: {
    "invalid-radius": "输入的半径超出有效范围…",
    "parse-error": "解析失败：{0}",
  },
})